package mh;

import java.awt.Canvas;
import java.awt.event.KeyEvent;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.text.ParseException;
import java.util.Hashtable;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Logger;

import javax.swing.JOptionPane;
import javax.xml.parsers.ParserConfigurationException;

import mh.interfaces.Constants;
import mh.log.MHLogger;
import mh.map.CachedResources;
import mh.map.Map;
import mh.map.TMXMapReader;
import mh.message.IncomingMessage;
import mh.message.MessageValidator;

import org.lwjgl.Sys;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;

/**
 * This is the first class of the game. The main hook of our game. This class with both act as a manager for the display
 * and central elaborator for the game logic.
 * 
 * Display management will consist of a loop that cycles round all entities in the game asking them to move and then
 * drawing them in the appropriate place. With the help of an inner class it will also allow the player to control the
 * main sprite.
 * 
 * As a elaborator it will be informed when entities within our game detect events it is in connection with the server.
 * 
 * @author Gianmarco Laggia
 */
public class MagicHogwarts extends Canvas implements GameWindowCallback, Constants {

	// =================================================== //
	//                   CONFIG ELEMENTS                   //
	// =================================================== //

	/**
	 * 
	 */
	private static final long							serialVersionUID		= 1L;
	/**
	 * The hashtable that contains each parameter. Parameters are read from a file
	 */
	private Hashtable<String, String>					params;
	/**
	 * The logger
	 */
	private static final Logger							LOG						= Logger.getLogger(MagicHogwarts.class.getName());

	// =================================================== //
	//                   GRAPHIC ELEMENTS                  //
	// =================================================== //

	/** The window that is being used to render the game */
	private final GameWindow							window;

	/** The normal title of the window */
	private final String								windowTitle				= "MagicHogwarts - Version (0.2a)";

	/** The tile map handler */
	private Map											mappa;

	// =================================================== //
	//                       TIMING                        //
	// =================================================== //

	/** The time at which the last rendering looped started from the point of view of the game logic */
	private long										lastLoopTime			= getTime();

	/** The time since the last record of fps */
	private long										lastFpsTime				= 0;

	/** The recorded fps */
	private int											fps;

	/** The time at which last fired a shot */
	private long										lastFire				= 0;

	/** The interval between our players shot (ms) TODO remove, read from each player */
	private final long									firingInterval			= 500;

	// ====================================================== //
	//               COMMUNICATIONS WITH SERVER               //
	// ====================================================== //

	/** The queue of incoming messages */
	private final LinkedBlockingQueue<IncomingMessage>	incomingMessageQueue;

	/** The TCP client listener */
	private ClientTCP									clientTCP;

	/** ClientTCP's thread */
	private Thread										clientTCPThread;

	/** The UDP client listener */
	private ClientUDP									clientUDP;

	/** ClientUDP's thread */
	private Thread										clientUDPThread;

	/** The message executor */
	private MessageExecutor								msgExecutor;

	/** Message executor's thread */
	private Thread										executorThread;

	/** Variable used to check if the server validate the connection */
	private final boolean								validLogin				= false;

	// ====================================================== //
	//                      GAME ELEMENTS                     //
	// ====================================================== //

	/** The entity representing the player */
	private PlayerEntity								player;

	/** The list of all the entities that exist in our game. Better to say in THIS map */
	//		private final ArrayList<EntityOld>			entities				= new ArrayList<EntityOld>();

	/** The list of entities that need to be removed from the game this loop */
	//		private final ArrayList<EntityOld>			removeList				= new ArrayList<EntityOld>();

	/** True if we're holding up game play until a key has been pressed FIXME */
	private final boolean								waitingForKeyPress		= true;

	/** True if game logic needs to be applied this loop, normally as a result of a game event */
	private boolean										logicRequiredThisLoop	= false;

	private SpellEntity									shot;

	/**
	 * Construct our game and set it running.
	 * 
	 */
	public MagicHogwarts() {

		// ************ LOG ************ //
		LOG.config("MagicHogwarts Client is starting.");
		// ***************************** //
		this.window = ResourceFactory.get().getGameWindow();
		new MessageValidator();
		this.incomingMessageQueue = new LinkedBlockingQueue<IncomingMessage>();

		try {
			this.params = Connectivity.readParamsFromFile("conf/mhconfig.txt", "=");
		} catch (FileNotFoundException e) {
			JOptionPane.showMessageDialog(null, "Error, file 'mhconfig.txt' not found.\nImpossible to load the params.\n- ERROR 001 -");
		} catch (IOException e) {
			JOptionPane.showMessageDialog(null, "Error while loading the game.\n- ERROR 002 -");
		}
		//decomment from here

		//				if (this.params != null) {
		//					if (this.params.size() < 3 || !this.params.containsKey("SERVER") || !this.params.containsKey("PORT") || !this.params.containsKey("NAME")) {
		//		
		//						// ************ LOG ************ //
		//						LOG.warning("Not enough params found. Shutting down...");
		//						// ***************************** //
		//		
		//						JOptionPane.showMessageDialog(null, "Error, not enough configuration params, shutting down.\n- ERROR 003 -");
		//						System.exit(ERROR);
		//					} else {
		//		
		//						String nickname = this.params.get("NAME");
		//						JTextField nicknameField = new JTextField();
		//						JPasswordField passwordField = new JPasswordField();
		//						if (!nickname.equalsIgnoreCase("NOT_REGISTERED")) {
		//							//					JDialog dialog = new JDialog();
		//							//					dialog.setModal(true);
		//							//					dialog.setTitle("Schermata di login");
		//							//					dialog.setBounds(10, 10, 100, 100);
		//							//					dialog.setVisible(true);
		//							//TODO Create a beautiful JDialog, with graphics, instead of this s*it of custom dialog.
		//							//					nickname = JOptionPane.showInputDialog("No Nickname avaible in the conf file.\nIf registered, insert your nickname");
		//							//					System.out.println(nickname);
		//							nicknameField.setText(nickname);
		//						}
		//						JPanel p = new JPanel();
		//						p.setLayout(new BoxLayout(p, BoxLayout.PAGE_AXIS));
		//						p.setPreferredSize(new Dimension(300, 100));
		//						p.setBorder(BorderFactory.createLineBorder(Color.BLACK));
		//						final JComponent[] inputs = new JComponent[] { p, new JLabel("Nickname"), nicknameField, new JLabel("Password"), passwordField };
		//						JOptionPane.showMessageDialog(null, inputs, "Login window", JOptionPane.PLAIN_MESSAGE);
		//						nickname = nicknameField.getText();
		//						try {
		//							Connectivity.writeParamsToFile("conf/mhconfig.txt", "=", "NAME", nickname, null);
		//						} catch (FileNotFoundException e) {
		//							JOptionPane.showMessageDialog(null, "Error, file 'mhconfig.txt' not found.\nImpossible to load the params.\n- ERROR 001 -");
		//							e.printStackTrace();
		//						} catch (IOException e) {
		//							e.printStackTrace();
		//						}
		//						char[] password = passwordField.getPassword();
		//		
		//						String host = this.params.get("SERVER");
		//						int port = Integer.parseInt(this.params.get("PORT"));
		//		
		//						// ************ LOG ************ //
		//						LOG.fine("Creating TCP Thread & socket");
		//						// ***************************** //
		//						this.clientTCP = new ClientTCP(this, host, port, nickname);
		//						this.clientTCPThread = new Thread(this.clientTCP);
		//		
		//						// ************ LOG ************ //
		//						LOG.fine("Connecting ClientTCP");
		//						// ***************************** //
		//						this.clientTCP.connect();
		//		
		//						// ************ LOG ************ //
		//						LOG.fine("Starting TCP Thread");
		//						// ***************************** //
		//						this.clientTCPThread.start();
		//		
		//						// ************ LOG ************ //
		//						LOG.fine("Creating MessageExecutor Thread");
		//						// ***************************** //
		//						this.msgExecutor = new MessageExecutor(this, clientTCP);
		//						this.executorThread = new Thread(msgExecutor);
		//		
		//						// ************ LOG ************ //
		//						LOG.config("Logging in...");
		//						// ***************************** //
		//						this.msgExecutor.login(nickname, password);
		//						
		//		//				this.clientUDP = new ClientUDP(this);
		//		//				this.clientUDPThread = new Thread(clientUDP);
		//		//				this.clientUDPThread.start();
		//					}
		//				} else {
		//					JOptionPane.showMessageDialog(null, "Could not connect to the server, runnin in non playing mode.");
		//				}

		//TO HERE
		CachedResources.singleton().start();
		this.startGame();

	}

	/**
	 * Start the listener of messages coming from the server.
	 */
	public void startExecutor() {
		// ************ LOG ************ //
		LOG.fine("Starting MessageExecutor");
		// ***************************** //
		this.executorThread.start();
	}

	/**
	 * Used to get the TCP Client
	 * 
	 * @return <code>ClientTCP</code> - The client
	 */
	//		 public ClientTCP getClientTCP() { return this.clientTCP; }

	/**
	 * Initialize the common elements for the game
	 */
	@Override
	public void initialise() {

		this.initEntities();
	}

	/**
	 * Start a fresh game, this should clear out any old data and create a new set.<br>
	 * This method can be used as a "reset".
	 */
	public void startGame() {
		this.window.setResolution(WINDOW_WIDTH, WINDOW_HEIGHT);
		this.window.setGameWindowCallback(this);
		this.window.setTitle(this.windowTitle);
		this.window.startRendering();
		this.initialise();
	}

	/**
	 * Initialize the starting state of the entities. Each entity will be added to the overall list of entities in the
	 * game.
	 */
	private void initEntities() {
		for (int i = 0; i < (WINDOW_WIDTH / 32); i++) {
			for (int j = 0; j < (WINDOW_HEIGHT / 32); j++) {
				// this.entities.add(new SpellEntity("sprites/griglia.gif", i, j, 0));
			}
		}

		this.player = new PlayerEntity("sprites/charset.png", CENTRAL_COLUMN, CENTRAL_ROW); //The sprite is ALWAYS in centre of the visible map

		TMXMapReader reader = new TMXMapReader();
		try {
			this.mappa = reader.readMap("/maps/prima.tmx", this.player);//currentRootDirectoryPath+"maps/sewers.tmx");
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (ParseException e) {
			e.printStackTrace();
		}
		//		this.mappa = new Map("mappa", this.player, 36, 28);
	}

	/**
	 * Notification from a game entity that the logic of the game should be run at the next opportunity (normally as a
	 * result of some game event)
	 */
	public void updateLogic() {
		this.logicRequiredThisLoop = true;
	}

	/**
	 * Notification that the player has died.
	 */
	// public void notifyDeath() {
	// this.message = this.gotYou;
	// this.waitingForKeyPress = true;
	// }

	/**
	 * Notification that an alien has been killed
	 */
	// public void notifyAlienKilled() {
	// // reduce the alient count, if there are none left, the player has won!
	//
	// this.alienCount--;
	//
	// if (this.alienCount == 0) {
	// this.notifyWin();
	// }
	//
	// // if there are still some aliens left then they all need to get faster, so
	//
	// // speed up all the existing aliens
	//
	// for (int i = 0; i < this.entities.size(); i++) {
	// Entity entity = this.entities.get(i);
	//
	// if (entity instanceof AlienEntity) {
	// // speed up by 2%
	//
	// entity.setHorizontalMovement(entity.getHorizontalMovement() * 1.02);
	// }
	// }
	// }

	/**
	 * Attempt to fire a shot from the player. Its called "try" since we must first check that the player can fire at
	 * this point, i.e. has he/she waited long enough between shots
	 */
	public void tryToCastSpell() {
		// check that we have waiting long enough to fire
		if ((System.currentTimeMillis() - this.lastFire) < this.firingInterval) {
			return;
		}

		// if we waited long enough, create the shot entity, and record the time.
		// TODO take values of Spell velocity and firing interval from the PLAYER!
		this.lastFire = System.currentTimeMillis();
		this.shot = this.mappa.addSpell("sprites/shot.gif", Mouse.getX(), (WINDOW_HEIGHT - Mouse.getY()));
	}

	/**
	 * Get the accurate system time
	 * 
	 * @return The system time in milliseconds
	 */
	public static long getTime() {
		return (Sys.getTime() * 1000) / Sys.getTimerResolution();
	}

	/**
	 * Notification that a frame is being rendered. Responsible for running game logic and rendering the scene.
	 */
	@Override
	public void frameRendering() {
		Display.sync(200);

		long delta = getTime() - this.lastLoopTime;
		this.lastLoopTime = getTime();
		this.lastFpsTime += delta;
		this.fps++;

		// update our FPS counter if a second has passed
		if (this.lastFpsTime >= 1000) {
			this.window.setTitle(this.windowTitle + " (FPS: " + this.fps + ")");
			this.lastFpsTime = 0;
			this.fps = 0;
		}

		//		if (this.logicRequiredThisLoop) {
		//			IncomingMessage msg = this.incomingMessageQueue.remove();
		//			System.out.println(msg.toString());
		//			this.logicRequiredThisLoop = false;
		//		}
		// cycle round asking each entity to move itself

		//		for (int i = 0; i < this.entities.size(); i++) {
		//			Entity entity = this.entities.get(i);
		//
		//			entity.move(delta,0); // TODO
		//		}

		// cycle round drawing all the entities we have in the game

		this.mappa.draw(delta);

		// brute force collisions, compare every entity against

		// every other entity. If any of them collide notify

		// both entities that the collision has occured

		//		for (int p = 0; p < this.entities.size(); p++) {
		//			for (int s = p + 1; s < this.entities.size(); s++) {
		//				Entity me = this.entities.get(p);
		//				Entity him = this.entities.get(s);
		//
		//				if (me.collidesWith(him)) {
		//					me.collidedWith(him);
		//					him.collidedWith(me);
		//				}
		//			}
		//		}
		if (this.shot != null) {
			//			if(this.shot.collidesWith(this.mappa.getOthers().others.get(0))){
			//				System.out.println("beccato");
			//			}
		}

		// if a game event has indicated that game logic should
		// be resolved, cycle round every entity requesting that
		// their personal logic should be considered.

		if (this.logicRequiredThisLoop) {
			//			for (int i = 0; i < this.entities.size(); i++) {
			//				//				Entity entity = this.entities.get(i);
			//				//				entity.doLogic();
			//			}

			this.logicRequiredThisLoop = false;
		}

		// resolve the movemfent of the ship. First assume the ship
		// isn't moving. If either cursor key is pressed then
		// update the movement appropraitely
		boolean leftPressed = this.window.isKeyPressed(KeyEvent.VK_A);
		boolean rightPressed = this.window.isKeyPressed(KeyEvent.VK_D);
		boolean upPressed = this.window.isKeyPressed(KeyEvent.VK_W);
		boolean downPressed = this.window.isKeyPressed(KeyEvent.VK_S);
		boolean moveOther = this.window.isKeyPressed(KeyEvent.VK_RIGHT);

		boolean moved = false;
		if ((leftPressed) && (!rightPressed)) {
			moved = this.mappa.move(delta, 1);
		} else if ((rightPressed) && (!leftPressed)) {
			moved = this.mappa.move(delta, 2);
		} else if ((upPressed) && (!downPressed)) {
			moved = this.mappa.move(delta, 3);
		} else if ((downPressed) && (!upPressed)) {
			moved = this.mappa.move(delta, 0);
		}
		if (moved) {
			//			this.clientTCP.send("PING:");
		}
		if (!upPressed && !downPressed && !leftPressed && !rightPressed) {
			this.mappa.stop();
			//			this.mappa.getOthers().getOthers().get(0).stop();
		}
		if (moveOther) {
			this.mappa.getOthers().getOthers().get(0).move(delta, 2);
		}
		if (Mouse.isButtonDown(0)) {
			// JOptionPane.showMessageDialog(new JFrame(), "EVENT OCCURRED Mouse schiaccied. coord: " + Mouse.getX() /
			// TILE_SIZE + " "
			// + (WINDOW_HEIGHT - Mouse.getY()) / TILE_SIZE, "A plain message",
			// JOptionPane.PLAIN_MESSAGE);
			//			this.tryToCastSpell();

		}
		//
		// // if we're pressing fire, attempt to fire
		//
		// if (firePressed) {
		// this.tryToFire();
		// }
		// } else {
		// if (!firePressed) {
		// this.fireHasBeenReleased = true;
		// }
		// if ((firePressed) && (this.fireHasBeenReleased)) {
		// this.waitingForKeyPress = false;
		// this.fireHasBeenReleased = false;
		// this.startGame();
		// }
		// }
	}

	/**
	 * Notifcation that the game window has been closed
	 */
	@Override
	public void windowClosed() {
		//TODOthis.clientTCP.disconnect();
		System.exit(0);
	}

	/**
	 * The entry point into the game. We'll simply create an instance of class which will start the display and game
	 * loop.
	 * 
	 * @param argv
	 *            The arguments that are passed into our game
	 */
	public static void main(String argv[]) {

		try {
			MHLogger.setup();
		} catch (IOException e) {
			JOptionPane.showMessageDialog(null, "Error in the setup of the logger.\n- ERROR 004 -");
		}
		// Check for updates of map
		// Check for updates of sprites
		// Here maybe we start a thread, the map are needed, sprites can be temporarily substituted by a standard tile
		// (maybe a question point?) so we can load the sprites while playing... The process that make the player wait
		// for having the map loaded should be friendly implemented with a new little window.
		new MagicHogwarts();
	}

	public void enqueueMessage(IncomingMessage msg) {
		if ((msg != null) && !"".equals(msg)) {
			this.incomingMessageQueue.add(msg);
			this.logicRequiredThisLoop = true;
		}
	}

	/**
	 * Returns the first message received from the server from the queue
	 * 
	 * @return A parsed message
	 * @throws InterruptedException
	 */
	public IncomingMessage getMessage() throws InterruptedException {
		return this.incomingMessageQueue.take();
	}

}
